package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.PhoneUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.CrossOrigin;


import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.nio.file.CopyOption;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static com.hmdp.utils.RedisConstants.LOGIN_CODE_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
@CrossOrigin//允许跨域请求
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private RedisTemplate redisTemplate;//注入redistemplate对象操作redis数据库
    @Resource
    private StringRedisTemplate stringRedisTemplate;//注入StringRedisTemplate对象操作redis数据库

    /**
     * 发送验证码
     *
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.业务健壮性判断
        if (phone == null || phone.length() != 11) {
            return Result.fail("请输入正确的手机号!!!");
        }
        //2.校验手机号格式,如果regex正则表达式校验不通过,则返回错误信息
        if (!phone.matches("^1[3-9]\\d{9}$")) {//^1[3-9]\d{9}$: 匹配13、14、15、16、17、18、19开头的手机号
            return Result.fail("手机号格式不正确!!!");
        }
        //3.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号不合法!!!");
        }
        //4.符合,生成随机6位的数字验证码
        String verifyCode = RandomUtil.randomNumbers(6);
        //5.保存验证码到session中
        /*session.setAttribute("code", verifyCode);*/
        //5.保存验证码到redis缓存中
        redisTemplate.opsForValue().set(LOGIN_CODE_KEY + ":" + phone, verifyCode);
        redisTemplate.expire(LOGIN_CODE_KEY + ":" + phone, Duration.ofSeconds(LOGIN_USER_TTL));
        //6.记录日志
        log.debug("手机号{}验证码{}", phone, verifyCode);

        //7.返回成功信息
        return Result.ok("验证码发送成功!!!");
    }

    /**
     * 验证码验证登录,注册
     *
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 0.业务健壮性判断
        if (loginForm == null) {
            return Result.fail("用户登入信息为空!!!");
        }

        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确!!!");
        }
        // 3.根据手机号查询用户
        //User user = lambdaQuery().eq(User::getPhone, phone).one();//User::getPhone通过反射调用UserMapper的getPhone方法,获取phone属性值
        User user = query().eq("phone", phone).one();
        // 2.校验验证码
        //取出session中的验证码和用户输入的验证码进行比较
        //取出密码和数据库中的密码进行比较
        //密码和验证码只要有一个,验证就通过
        String loginCode = loginForm.getCode(); // 用户输入的验证码
        String verifyCode = (String) redisTemplate.opsForValue().get(LOGIN_CODE_KEY + ":" + phone); // redis中的验证码

        // 业务健壮性判断
        if (verifyCode == null) {
            log.debug("验证码不存在, sessionCode=null");
            return Result.fail("验证码不存在!!!");
        }

        if (!loginCode.equals(verifyCode)) {
            log.debug("验证码错误, sessionCode={}, loginCode={}", verifyCode, loginCode);
            return Result.fail("验证码错误!!!");
        }
        // 4.用户不存在,创建新用户; 用户存在,把用户信息放入session
        if (user == null) {
            user = createNewUser(loginForm, phone);
        }
        // 5.登录成功,设置session
        /*session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*/
        //生成token.时间戳字符串,token=userId:时间戳
        String token = user.getId() + ":" + LocalDateTime.now().getNano();
        String key = RedisConstants.LOGIN_USER_KEY + ":" + token;
 /*       UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        String jsonStr = JSONUtil.toJsonStr(userDTO);
        //5.登入成功,将用户信息保存到
        redisTemplate.opsForValue().set(RedisConstants.LOGIN_USER_KEY + ":" + token, jsonStr);*/
        //用户对象转换为dto,返回时不包括敏感信息
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //把对象转为HashMap存放到redis的hash键值对中(键和值都是字符串)
        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //5.登入成功,将用户信息保存到
        redisTemplate.opsForHash().putAll(key, stringObjectMap);
        //设置TTL有效期
        redisTemplate.expire(key, Duration.ofMillis(LOGIN_USER_TTL));
        //下面为测试使用代码
        UserDTO userDTO1 = BeanUtil.mapToBean(stringObjectMap, UserDTO.class, true, CopyOptions.create().setIgnoreNullValue(true));
        log.info("userDTO1:{}", userDTO1);
        // 6.返回成功信息
        return Result.ok(token);
    }

    /**
     * 返回当前登录用户信息
     *
     * @return
     */
    @Override
    public Result getCurrentLoginUser() {
        //1.从线程域变量中取出用户信息
        UserDTO user = (UserDTO) ThreadLocalUtil.get();
        //2.判断用户是否存在
        if (user == null) {
            return Result.fail("用户登录状态异常,请重新登录!!!!");
        }
        //3.查询用户信息
        User user1 = query().eq("id", user.getId()).one();
        //4.转换为DTO返回
        UserDTO userDTO = BeanUtil.copyProperties(user1, UserDTO.class);//把user1中的属性值拷贝到user2中
        return Result.ok(userDTO);
    }

    /**
     * 当前登入用户签到
     */
    @Override
    public Result sign() {
        //1.获取当前登录用户
        UserDTO user = (UserDTO) ThreadLocalUtil.get();
        Long userId = user.getId();
        //2.签到
        LocalDateTime now = LocalDateTime.now();
        String signDateTimeStr = now.format(DateTimeFormatter.ofPattern("yyyyMM:"));
        String key = RedisConstants.USER_SIGN_KEY + userId + signDateTimeStr;
        //获取本月天数
        int dayOfMonth = now.getDayOfMonth();
        //获取本月签到日期
        int offset = now.getDayOfMonth() - 1;//redis的bit位从0开始,所以需要-1
        if (Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, offset))) {
            return Result.fail("您今天已经签到过了!!!");
        }
        //签到成功,设置签到日期
        Boolean b = stringRedisTemplate.opsForValue().setBit(key, offset, true);
        if (b) {
            log.info("签到成功, userId={}, signDateTimeStr={}", userId, signDateTimeStr);
        }
        //返回结果
        return Result.ok();//签到成功
    }

    /**
     * 统计这几个月的签到次数和连续签到天数
     *
     * @return
     */
    @Override
    public Result signCount() {
        //1.从redis中获取用户签到信息的key
        //获取当前登录用户
        UserDTO user = (UserDTO) ThreadLocalUtil.get();
        Long userId = user.getId();
        //签到
        LocalDateTime now = LocalDateTime.now();
        String signDateTimeStr = now.format(DateTimeFormatter.ofPattern("yyyyMM:"));
        String key = RedisConstants.USER_SIGN_KEY + userId + signDateTimeStr;
        //2.从redis中获取用户签到信息(BitMap的签到记录)
        //获取本月天数
        int dayOfMonth = now.getDayOfMonth();
        //获取这个月到今天的签到记录,，返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
        List<Long> result = stringRedisTemplate.opsForValue()
                .bitField(key, BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType
                                .unsigned(dayOfMonth))
                        .valueAt(0));
        //如果result为空，说明用户没有签到过，返回0
        if (result == null || result.isEmpty()) {
            log.info("用户{}没有签到过", userId);
            return Result.ok(0);
        }
        //3.获取本月签到结果
        Long signNum = result.get(0);
        //如果签到结果为0，说明用户没有签到过，返回0
        if (signNum == null || signNum == 0) {
            log.info("用户{}没有签到过", userId);
            return Result.ok(0);
        }
        //4.统计连续签到天数和总签到次数
        int count = 0;//统计连续签到天数的计数器
        while (true) {
            if ((signNum & 1) == 0) {
                //签到记录为0，说明用户这天没有签到过,断签了，返回0
                break;

            } else {
                count++;//连续签到天数计数器加1
            }
            signNum >>>= 1;//signNum>>>=1和signNum>>1的区别是,signNum>>>=1是无符号右移,也就是说,如果signNum的最高位为1,则向左移位,否则,向右移位,而signNum>>1是有符号右移,也就是说,如果signNum的最高位为1,则向右移位,否则,向左移位
        }
        //5.返回结果
        return Result.ok(count);
    }

    // 创建新用户的方法
    private User createNewUser(LoginFormDTO loginForm, String phone) {
        User newUser = new User();
        newUser.setPhone(phone);
        newUser.setPassword(Optional.ofNullable(loginForm.getPassword()).filter(p -> !p.isEmpty()).orElse(DigestUtils.md5Hex("123456")));
        newUser.setIcon("https://my-bucket-liufubiao.oss-cn-fuzhou.aliyuncs.com/1e87b74b-fba1-4672-88a3-17a4eb7cc91e.png");
        newUser.setNickName("用户" + RandomUtil.randomString(6));
        newUser.setCreateTime(LocalDateTime.now());
        newUser.setUpdateTime(LocalDateTime.now());
        save(newUser);
        return newUser;
    }
}
